package planning.heuristic;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;

import planning.util.MachineUsageData;
import planning.util.PlanIOHandler;
import provisioning.DPS;
import provisioning.Monitor;
import provisioning.OptimalProvisioningSystemForHeterogeneousMachines;

import commons.cloud.MachineType;
import commons.cloud.Provider;
import commons.cloud.User;
import commons.config.Configuration;
import commons.io.Checkpointer;
import commons.sim.SimpleSimulator;
import commons.sim.components.LoadBalancer;
import commons.sim.jeevent.JEEventScheduler;
import commons.sim.util.SimulatorProperties;

/**
 * This class represents the heuristic that plans reservation evaluating machine utilization
 * over a planning period (e.g., one year)
 * @author David Candeia
 *
 */
public class HistoryBasedHeuristic implements PlanningHeuristic{

	private Map<Provider, Map<MachineType, Integer>> plan;
	private MachineUsageData machineData;
	private final Monitor monitor;

	private Map<MachineType, Map<Long, Double>> hoursUsed;//map used to save CPU consumption information during the planning period

	/**
	 * Default constructor. 
	 * @param scheduler Event scheduler
	 * @param monitor Application monitor (DPS)
	 * @param loadBalancers Load balancers used in simulation
	 */
	public HistoryBasedHeuristic(JEEventScheduler scheduler, Monitor monitor, LoadBalancer[] loadBalancers){
		this.monitor = monitor;
		
		this.plan = new HashMap<Provider, Map<MachineType,Integer>>();
		
		try {
			machineData = PlanIOHandler.getMachineData();
		} catch (IOException e) {
		} catch (ClassNotFoundException e) {
		}
		if(machineData == null){
			machineData = new MachineUsageData();
		}
		
		this.hoursUsed = new HashMap<MachineType, Map<Long,Double>>();
	}
	
	@Override
	public void findPlan(Provider[] cloudProviders, User[] cloudUsers) {
		
		//Simulating ...
		DPS dps = (DPS) this.monitor;
		
		SimpleSimulator simulator = (SimpleSimulator) Checkpointer.loadApplication();
		
		dps.registerConfigurable(simulator);
		
		simulator.start();
		
		//Calculating machines use data
		LoadBalancer[] loadBalancers = simulator.getTiers(); 
		calculateMachinesUsage();
		Configuration config = Configuration.getInstance();
		
		if(Checkpointer.loadSimulationInfo().isFinishDay()){//Simulation finished!
			
			//Creates reservation plan
			calculateMachinesToReserve(config);
			
			Checkpointer.clear();
			PlanIOHandler.clear();
			try {
				PlanIOHandler.createPlanFile(this.plan);
			} catch (IOException e) {
				throw new RuntimeException(e);
			}
			
			
		}else{//Persist data to other round
			
			persisDataToNextRound(loadBalancers, config);
		}
	}
	
	/**
	 * This method calculates the utilization target at which each machine type reservation is profitable
	 * @param providers IaaS provider
	 * @param planningPeriod Period being planned 
	 * @return Percentage of minimum utilization for each machine type
	 */
	private Map<Provider, Map<MachineType, Double>> findReservationTargets(Provider[] providers, long planningPeriod) {
		Map<Provider, Map<MachineType, Double>> targets = new HashMap<Provider, Map<MachineType,Double>>();
		
		for(Provider provider : providers){
			MachineType[] machineTypes = provider.getAvailableTypes();
			Map<MachineType, Double> typesLimits = new HashMap<MachineType, Double>();//utilization targets per machine type
			
			for(MachineType type : machineTypes){
				double yearFee = provider.getReservationOneYearFee(type);
				double reservedCpuCost = provider.getReservedCpuCost(type);
				double onDemandCpuCost = provider.getOnDemandCpuCost(type);
				
				long minimumHoursToBeUsed = Math.round(yearFee / (onDemandCpuCost - reservedCpuCost));
				double usageProportion = 1.0 * minimumHoursToBeUsed / (planningPeriod * 24);
				
				typesLimits.put(type, usageProportion);
			}
			targets.put(provider, typesLimits);
		}
		
		return targets;
	}

	private void calculateMachinesToReserve(Configuration config) {
		long planningPeriod = config.getLong(SimulatorProperties.PLANNING_PERIOD);
		
		//Updating machines consumption
		Map<MachineType, Map<Long, Double>> hoursUsed = updateConsumption();
		
		//Sorting providers
		Provider[] providers = sortProviders();
		
		Map<Provider, Map<MachineType, Double>> usageMap = new HashMap<Provider, Map<MachineType,Double>>();
		Map<Provider, Map<MachineType, Double>> utilizationTargets = findReservationTargets(providers, planningPeriod);
	
		this.hoursUsed = new HashMap<MachineType, Map<Long, Double>>(hoursUsed);
		
		//Checking machines which had utilization higher than the minimum utilization target
		for(MachineType machineType : hoursUsed.keySet()){
			Map<Long, Double> machines = hoursUsed.get(machineType);
			
			ArrayList<Long> sortedMachines = new ArrayList<Long>(machines.keySet());
			Collections.sort(sortedMachines);//Sorting amount of machines used in ascending order
			
			long alreadyReserved = 0;

			//Selecting providers for other amounts of machines
			for(int machinesIndex = 0; machinesIndex < sortedMachines.size(); machinesIndex++){
				Long currentNumberOfMachines = sortedMachines.get(machinesIndex);
				Double currentUsed = machines.get(currentNumberOfMachines);
			
				if(currentUsed != null && currentUsed > 0){
					
					double currentUtilization = currentUsed / (machineType.getTotalCapacity() * planningPeriod * 24);
					
					Map<MachineType, Double> target = utilizationTargets.get(providers[0]);
					
					if(currentUtilization >= target.get(machineType)){//Checking if current utilization is greater than the target 
						
						Provider chosenProvider = providers[0];
						//Checking if it is valid to change from first to second provider, and so on
						for (int  i = 1; i < providers.length; i++){
							double hoursChangeTarget = (providers[i].getReservationOneYearFee(machineType) - providers[i-1].getReservationOneYearFee(machineType))
							/ (providers[i-1].getReservedCpuCost(machineType) - providers[i].getReservedCpuCost(machineType));
							double utilizationChangeTarget = hoursChangeTarget / (planningPeriod * 24);
							
							if(currentUtilization >= utilizationChangeTarget){//Change providers
								chosenProvider = providers[i];
							}else{//keep previous provider
								break;
							}
						}
						
						Map<MachineType, Double> utilizationPerType = usageMap.get(chosenProvider);
						if(utilizationPerType == null){
							utilizationPerType = new HashMap<MachineType, Double>();
						}
						Double previousUtilization = utilizationPerType.get(machineType);
						if(previousUtilization == null){
							previousUtilization = new Double(0);
						}
//						utilizationPerType.put(machineType, previousUtilization + (currentNumberOfMachines - alreadyReserved) * currentUtilization);
						utilizationPerType.put(machineType, previousUtilization + (currentNumberOfMachines - alreadyReserved) * 1.0);
						usageMap.put(chosenProvider, utilizationPerType);
						alreadyReserved = currentNumberOfMachines;
					}
				}
			}
			
			//Calculating the amount of machines to reserve for each provider
			for(int i = 0; i < providers.length; i++){
				Provider provider = providers[i];
				
				Map<MachineType, Double> typeUsageMap = usageMap.get(provider);
				if(typeUsageMap != null && !typeUsageMap.isEmpty()){
					
					int toReserve = 0;
					Double amountUsed = typeUsageMap.get(machineType);
//					if(i == 0){
//						toReserve = (int) Math.round(typeUsageMap.get(machineType) / utilizationTargets.get(provider).get(machineType));
//					}else{
//						double hoursChangeTarget = (providers[i].getReservationOneYearFee(machineType) - providers[i-1].getReservationOneYearFee(machineType))
//								/ (providers[i-1].getReservedCpuCost(machineType) - providers[i].getReservedCpuCost(machineType));
//						double utilizationChangeTarget = hoursChangeTarget / (planningPeriod * 24);
//						
//						toReserve = (int) Math.round(typeUsageMap.get(machineType) / utilizationChangeTarget);
//					}
					if(amountUsed != null){
						toReserve = (int) Math.round(amountUsed);
						
						Map<MachineType, Integer> reservation = this.plan.get(provider);
						if(reservation == null){
							reservation = new HashMap<MachineType, Integer>();
						}
						reservation.put(machineType, toReserve);
						this.plan.put(provider, reservation);
					}
				}
			}
		}
	}
	
	/**
	 * This method iterates through machines usedin simulation in order to update cumulative consumption (e.g., If 15 machines were used for 
	 * 10 hours, 10 machines were also used for 10 hours)
	 * @param highestNumberOfMachinesUsedPerType
	 * @return A map containing for each machine type the amount of machines and their respective amoung of hours consumed
	 */
	private Map<MachineType, Map<Long, Double>> updateConsumption() {
		
		Map<MachineType, Long> highestNumberOfMachinesUsedPerType = new HashMap<MachineType, Long>();
		Map<MachineType, Map<Long, Double>> hoursUsed = this.machineData.getMachineUsagePerType();
		
		//Updating hours used
		for(MachineType machineType : hoursUsed.keySet()){
			Map<Long, Double> usageData = hoursUsed.get(machineType);
			Map<Long, Double> newUsageData = new HashMap<Long, Double>();
			
			ArrayList<Long> numberOfMachinesList = new ArrayList<Long>(usageData.keySet());
			Collections.sort(numberOfMachinesList);
			Collections.reverse(numberOfMachinesList);
			
			highestNumberOfMachinesUsedPerType.put(machineType, numberOfMachinesList.get(0));
			
			newUsageData.put(numberOfMachinesList.get(0), usageData.get(numberOfMachinesList.get(0)));//First value
			
			for(Long numberOfMachinesUsed : numberOfMachinesList){//Iterating from the highest number of machines to the lowest one
				double higherUsed = usageData.get(numberOfMachinesUsed);
				
				for(long i = numberOfMachinesUsed - 1; i > 0; i--){//If 15 machines were used for 10 hours, 10 machines were also used for 10 hours ...
					Double lowerUsed = (newUsageData.get(i) != null) ? newUsageData.get(i) : usageData.get(i);
					
					if(lowerUsed != null){
						newUsageData.put(i, lowerUsed+higherUsed);
					}
				}
			}
			
			hoursUsed.put(machineType, newUsageData);
		}
		return hoursUsed;
	}
	
	/**
	 * This method sorts providers according to their minimum utilization targets
	 * @return An array containing providers ordered according to their minimum utilization targets 
	 */
	private Provider[] sortProviders() {
		Provider[] providers = Checkpointer.loadProviders();
		Arrays.sort(providers);
				
		return providers;
	}

	/**
	 * Saving current simulation day information
	 * @param loadBalancers
	 * @param config
	 */
	private void persisDataToNextRound(LoadBalancer[] loadBalancers,
			Configuration config) {
		try {
			Checkpointer.save();
			Checkpointer.dumpMachineData(this.machineData);
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}
	
	/**
	 * Retrieving amount of machines used by DPS.
	 */
	private void calculateMachinesUsage() {
		
		//TODO: Change this to collect usage for all machine types
		LinkedList<Integer> usage = ((OptimalProvisioningSystemForHeterogeneousMachines) this.monitor).machinesPerHour;
		for(Integer numberOfMachines : usage){
			machineData.addUsage(MachineType.M1_SMALL, numberOfMachines);
		}
	}

	@Override
	public double getEstimatedProfit(int period) {
		return 0;
	}

	@Override
	public Map<Provider, Map<MachineType, Integer>> getPlan(User[] cloudUsers) {
		return this.plan;
	}
}
